Linux Fu: un uso extraño para Fork() |Hackaday

2022-09-03 11:10:25 By : Ms. SW LONGESEN

Si eres fanático de Star Trek, probablemente recordarás la frase "Tienes que aprender por qué las cosas funcionan en una nave estelar".La verdad es que, en la mayoría de los episodios, saber cómo anular la consola de otro barco o hacer pólvora no fue muy útil, pero cuando lo hizo, realmente salvó el día.Linux es muy parecido a eso.Hay algunas cosas que probablemente no necesites saber muy a menudo, pero cuando las necesitas, hacen una gran diferencia.En esta publicación en particular, quiero ver un uso extraño de la llamada al sistema de bifurcación.Para muchos propósitos, nunca necesitará conocer este uso irregular en particular.Pero cuando lo necesites, realmente lo vas a necesitar.En realidad, esto se basa en un antiguo cliente mío que usaba Unix para ejecutar un informe masivo y muy crítico todos los días.El informe tenía muchas matemáticas ya que estaban tratando de optimizar algo y luego generar muchos informes.En aquellos días, la salida del informe era en papel viejo de barra verde en una impresora de línea.El problema era que el informe tardaba unas 14 horas en ejecutarse, incluidas las impresiones.Si alguien descubría algo incorrecto, no había tiempo para ejecutar el informe nuevamente porque el informe del día siguiente tendría que comenzar antes de que terminara la segunda ejecución.El cliente tenía un grupo de programadores de Windows y, en ese momento, no había nada realmente análogo a una llamada de bifurcación real en Windows.Miré el código y me di cuenta de que probablemente la mayor parte del código estaba esperando para imprimir la salida.La computadora tenía varias CPU y había varias impresoras, pero ese programa estaba colgado en una impresora.Había una gran cantidad de datos, por lo que escribirlos en una base de datos y luego ejecutar diferentes informes no era una buena opción.La respuesta fue usar el poder del tenedor.Con un cambio en el código que tomó menos de 30 minutos, el informe se ejecutó en cinco horas.Estaban muy contentos.Entonces, ¿cómo lo hice?La respuesta está en cómo funciona el tenedor.Casi cada vez que ve una bifurcación, ve algún tipo de llamada ejecutiva para iniciar un nuevo programa.Entonces, si piensas en fork, probablemente pienses que es parte de cómo comienzas un nuevo programa y, la mayoría de las veces, eso es cierto.La llamada, sin embargo, hace algo muy extraño.En realidad, copia todo el proceso en ejecución en un nuevo proceso.A continuación, ejecuta el nuevo proceso.Por supuesto, el proceso original también se está ejecutando.Normalmente, cuando ves un tenedor, se ve así:En otras palabras, el valor de retorno de la bifurcación es cero para un proceso secundario y algo más para el proceso principal.Algunos de los primeros sistemas Unix realmente copiaron todo en el proceso de ejecución.Sin embargo, eso es realmente ineficiente, especialmente cuando la mayoría de las veces carga inmediatamente un nuevo programa.Los sistemas modernos utilizan la semántica COW o Copy On Write.Eso significa que el nuevo proceso obtiene lo que equivale a un puntero a la memoria del proceso original y solo copia cantidades relativamente pequeñas de memoria cuando el programa secundario o principal realiza cambios en esa región de la memoria.Esto es bueno para cosas como los espacios de instrucción que no deberían cambiar de todos modos, ya que muy pocas personas todavía escriben código automodificable.Eso significa que justo después de una llamada de bifurcación, tanto el padre como el hijo ven exactamente los mismos datos, pero los cambios que hagan no se reflejarán en el otro lado.Para el extenso informe de mi cliente, el programa estaba principalmente vinculado a E/S.Sin embargo, cada informe también tenía algunas matemáticas bastante complejas que lo acompañaban, además de todas las matemáticas necesarias para llegar al punto en que cada informe podía ejecutarse.En lugar de ejecutarlo todo en un solo proceso, dividí el programa en varias partes.La primera pieza hizo tanta matemática como pudo que se aplicó a casi todo.Luego, el programa llamó a fork un montón de veces y cada niño comenzó un informe que hizo un poco más de matemáticas solo para sí mismo y reclamó una impresora para escribir la salida.Dado que la CPU tenía múltiples procesadores, todo se aceleró.El informe tres no tuvo que esperar a que se completaran los informes uno y dos.Todos pudieron manejar las impresoras a la vez.Fue una victoria general y casi no tomó tiempo hacer esta solución.Por supuesto, no todos los problemas permitirán una solución como esta.Pero dar a cada proceso de informe una copia de memoria de los datos fue muy rápido en comparación con leerlos desde un archivo o una base de datos.Los datos no cambiaron después de que comenzaron los informes, por lo que el consumo real de memoria tampoco fue tan malo.Entonces, ¿es realmente tan simple?Está.El único problema ahora es que con las máquinas modernas, es difícil encontrar un problema simple para demostrar la técnica.Finalmente me decidí por hacer algo simple, pero haciendo mucho.Mi tarea inventada: llenar una matriz realmente grande de números de punto flotante de doble precisión con algunos datos inventados pero predecibles y luego encontrar el promedio.Por realmente grande me refiero a 55 millones de entradas o más.Creé un programa que puede hacer el trabajo de dos maneras.Primero, simplemente lo hace de la manera más simple posible.Un bucle recorre cada elemento de la matriz, los suma y divide al final.En mi máquina, ejecutar esto varias veces toma un promedio de aproximadamente 458 milisegundos, usando el comando de tiempo para averiguarlo.El programa también puede aceptar un parámetro F en la línea de comando.Cuando eso está en efecto, la configuración es la misma, pero una bifurcación crea dos procesos para dividir la matriz por la mitad y encontrar el promedio de cada mitad.No quería que el niño se comunicara con el proceso, pero eso es posible, por supuesto.En cambio, solo tiene que leer los dos promedios, sumarlos y dividirlos por dos para obtener el promedio real.No quería agregar la sobrecarga para comunicar el resultado, pero sería bastante fácil de hacer.¿El tiempo para que se ejecute la versión de la bifurcación?Aproximadamente 395 milisegundos.Por supuesto, sus resultados variarán, y aunque aproximadamente 60 milisegundos no parece mucho, muestra que tener dos procesos trabajando juntos puede permitir que varios núcleos funcionen al mismo tiempo.Cuanto mayor sea la matriz, mayor será el ahorro de tiempo.Por ejemplo, establecer el tamaño en 155 256 000 mostró un ahorro de alrededor de 150 milisegundos.Por supuesto, estos tiempos no son científicos y hay muchos factores a considerar, pero los datos muestran claramente que dividir el trabajo entre dos procesos funciona más rápido.El código es sencillo.El trabajo no es difícil, solo hay mucho.Ahora que sabes cómo funciona realmente el tenedor.Especie de.Hay muchos matices sobre qué controles se le pasan al niño y cuáles no.Y, como dije, no necesitarás esto muy a menudo.Pero hay ocasiones en las que realmente salvará el día.Si desea una mirada de alto nivel a la multitarea, pruebe con un Linux Fu más antiguo.O consulte la herramienta GNU Parallel.Debo estar envejeciendo, la cantidad de veces que usé `fork()` supera con creces la cantidad de veces que lo usé combinado con `exec()` 😅 Es lo que hacíamos antes de que los subprocesos múltiples fueran un lugar común.Por curiosidad, ¿cuándo se convirtieron los subprocesos (con memoria compartida, frente a los procesos independientes con memoria independiente) en *Nix, si no siempre estuvieron ahí?Siempre me ha parecido extraño que la biblioteca pthreads se sienta como si estuviera conectada, y tengo la clara impresión de que simplemente usar fork() y varias formas de IPC era la forma de hacer paralelismo "en ese entonces".Posix Threads (pthread) se estandarizó en 1995, así que supongo que no por mucho tiempo.https://en.wikipedia.org/wiki/PthreadsExactamente correcto.Y recuerde que esa era la era de las variantes de Unix de código cerrado patentadas, por lo que pthreads era a menudo un complemento de software de costo adicional.¿No por mucho tiempo?Eso fue hace 27 años... ¿Ni siquiera recuerdo qué distribuciones existían en ese entonces?¿Qué computadora tenía?Además, pocas cosas de computadora de entonces todavía están disponibles o son relevantes.Slackware, redhat y debian 1.0: probé los tres en un 486 :)Los subprocesos, o "procesos ligeros", se volvieron comunes en la década de 1980, posiblemente antes, pero no había una forma estándar de hacerlo en todos los sistemas operativos.Hay algunos lenguajes que han incorporado soporte para subprocesos, aunque a veces lo llaman tareas (ver Ada).Las ideas para la bifurcación y unión, junto con la sincronización entre subprocesos, la transparencia de núcleos simples frente a múltiples (ejecución paralela real frente a alternancia secuencial) y la paralelización automática (ver Fortran) han estado dando vueltas desde la década de 1970 cuando estaba en la universidad.Incluso sin el soporte del sistema operativo, las personas lograron admitir múltiples subprocesos en sistemas monoprocesador (escribí el mío propio para MS DOS, Atari ST (TOS) y algunos otros, pero no eran portátiles. También había productos comerciales que proporcionaban esto. a los desarrolladores de software. Eventualmente, las cosas se calmaron y se convirtió en algo común compatible con muchos sistemas, y los hilos POSIX estuvieron disponibles en muchos sistemas. Aún así, hay, o había, una variedad de otras bibliotecas de hilos e interfaces en uso.Sí.Este no es un uso extraño.Es un uso completamente estándar que cualquier persona con algunos meses de experiencia en programación de Unix ya debería tener muy en cuenta.Actualmente tengo un código en producción que hace exactamente esto también.En PHP de todos los idiomas incluso.Súper confiable y a prueba de balas.¡Hola Al!Parece que hubo un poco de traducción de html cuando se agregó su código de muestra a su artículo.Algunas personas pueden encontrar esto un poco confuso.¡Buen artículo por cierto!Ley de Amdahl: “la mejora general del rendimiento que se obtiene al optimizar una sola parte de un sistema está limitada por la fracción de tiempo en que se usa realmente la parte mejorada”.Luego estaba el otro tipo que demostró que agregar más de la cantidad óptima de procesos paralelos al programa comienza a ralentizarlo, incluso si tiene los núcleos de CPU para ejecutarlos, porque la comunicación y la coordinación entre las tareas comienzan a consumir tiempo de cómputo. .Para muchos problemas cotidianos, el número óptimo de unidades paralelas en términos de ambas leyes es sorprendentemente bajo, alrededor de 10-20.¿Tienes algún enlace para respaldar lo que dices?El comportamiento observado por mí y por Phoronix es que agregar trabajos más allá del número de núcleos proporciona una aceleración porque las canalizaciones permanecen llenas todo el tiempo.Si el número de trabajos es igual al número de núcleos, las canalizaciones no permanecerán llenas.El kernel solo puede optimizar IO que conoce, por lo que enviar tantas solicitudes como sea posible al kernel lo más rápido posible es bueno porque genera más oportunidades para la optimización.Tuberías llenas!Las ralentizaciones de las que está hablando se solucionaron en el kernel de Linux hace décadas cuando Oracle, etc., comenzaron a usar Linux como plataforma para sus servidores.Ejecutamos miles de procesos en servidores con terabytes de RAM y el rendimiento es impresionante.Lo siento, estoy tratando de recordar cuál era el nombre del tipo, pero me estoy quedando corto.Estaban probando algoritmos en máquinas de hasta 128 núcleos y descubrieron que para muchas tareas comenzaban a obtener rendimientos decrecientes y luego rendimientos adversos después de unos 24 núcleos.> Ejecutamos miles de procesosSí, pero procesos independientes.El problema consistía en dividir una sola tarea de N formas, y la cantidad de comunicación y logística que se necesita para coordinar una tarea colaborativa de N formas.Puedo recordar mucho tiempo atrás, antes de los módulos del kernel, compilando con frecuencia el kernel de Linux.Esto tomó mucho tiempo en esos días, por lo que siempre buscábamos optimizaciones.Una vez que los sistemas de dos y cuatro núcleos estuvieron disponibles, comenzamos a experimentar con hilos de compilación.Hasta el día de hoy recuerdo la "fórmula" que encontramos para ofrecer los tiempos de compilación más cortos.Subprocesos = CPU x 2 +1.Ahora, no probaría esto hoy en sistemas con 16,32,64 núcleos, etc. Pero para los sistemas de 2 y 4 CPU del día, demostró ser consistentemente la mejor optimización para tiempos de compilación más rápidos.Mi experiencia es que depende.Las tareas de CPU alta y (quizás) de ancho de banda de memoria alto tienden a funcionar mejor si mantiene la cantidad de subprocesos de CPU.Las tareas que involucran tráfico de disco o de red pueden requerir más subprocesos o procesos.Pero requiere pruebas en el sistema en cuestión.En una instancia gratuita de Google Cloud e2, encontré que un generador de gráficos era adecuado (a pesar de los dos subprocesos, estaba en un núcleo), pero podíamos usar 16 subprocesos para las solicitudes a tantos nodos de datos.Sin embargo, ese número era menor cuando era un f1-micro, porque tenía menos RAM.Agregar más RAM nos permite evitar el desalojo de los archivos de la base de datos.De manera similar, para PostgreSQL, tendemos a establecer el máximo de procesos activos de modo que use la cantidad de subprocesos como límite para los procesos de manejo de consultas en total (sin considerar otros procesos en segundo plano), y un paralelismo máximo de modo que un solo proceso pueda generar tantos procesos backend para llenar todos los núcleos, pero no los subprocesos.Esto podría no ser completamente óptimo si hay acceso al disco, pero en ese caso es probable que sea el cuello de botella de todos modos.A esto añadiré: ¡medidlo!El hecho de que algo "parece" que tiene un paralelismo de N vías no significa que dividir aún más no sea una victoria.A veces está bien detener un núcleo en espera de otro solo para poder usar ambos cachés L1.El caché L1 tiene una memoria de aproximadamente 0,5 nanosegundos, pero no hay mucho.Más núcleos -> más cachés -> más velocidad.Para ser claros, no estoy en desacuerdo con Dude, solo menciono un truco sucio que me explotan un par de veces.:-)A pesar de que los procesadores de un solo núcleo ya casi no existen desde hace más de 10 años, todavía hay muchos programas que no logran hacer un uso adecuado de ellos.Compré un Ryzen 5600G hace unos meses y cada vez que mi PC se siente lenta y miro el administrador de tareas, entonces 11 subprocesos de esta PC de 6 núcleos y 12 subprocesos están inactivos.Agregar un -j12 para hacer puede ayudar mucho al compilar cosas, pero no compilo mucho más allá de las cosas de uC, que generalmente se completan en uno o dos segundos de todos modos.Acabo de echar un vistazo a https://www.cpubenchmark.net/high_end_cpus.html y creo que vale la pena mencionar que ahora hay un procesador que ha superado la calificación de 100 000 passmark.Es un procesador de 64 núcleos, e incluso si lo tuviera, probablemente no haría ninguna diferencia para mí debido al rendimiento de un solo hilo que no ha podido mantenerse al día con la ley de Moore durante varios años.De todos modos, la ley de Moore no tiene nada que ver con el rendimiento del subproceso.Se trata del tamaño más económico de un chip de silicio, medido por la cantidad de transistores que caben en él.Llamarlo "Ley de Moore" en este punto es simplemente una tontería.Nunca fue una “ley”, solo una “observación” y no muy buena.Esperar que continúe para siempre es un pensamiento mágico.Ambos tienen toda la razón y tuve una gran pérdida de cerebro al escribirlo de esa manera.Pero el punto que quería hacer sigue en pie.el rendimiento de un solo subproceso se ha estancado en su mayoría y, aunque todavía se puede ganar algo de rendimiento en esa área, el camino a seguir es hacer un mayor uso de subprocesos múltiples y falta mucho software en ese sentido y deja el hardware inactivo.Muchos problemas no se pueden paralelizar, o solo con una gran complejidad.tuConcuerdo completamente.El software comercial moderno y los sistemas operativos son basura en el paralelismo, al igual que la mayoría de los entornos de desarrollo.Linux no es mucho mejor con algunas optimizaciones clave subyacentes aún por realizar debido a los grandes cambios que requerirían.Además, las universidades no están enseñando (muy bien de todos modos) métodos de programación que resulten en un paralelismo altamente optimizado en el código final.En mi opinión, es porque todavía no hemos descubierto cómo hacerlo bien.Una vez más, en mi opinión, será necesario ir más allá de C++ y otros lenguajes comunes y hacia nuevos entornos de desarrollo y bibliotecas de sistemas diseñadas para maximizar el paralelismo y el uso del núcleo.Desafortunadamente, esto está en conflicto directo con la manía verde y el impulso para reducir el consumo de energía en todo.Como la mayoría de las manías, esto se basa en suposiciones estúpidas.En este caso, la idea de que el consumo máximo de energía (núcleos máximos utilizados) es peor que el consumo de energía del área bajo la curva.En otras palabras, "Dios mío, el sistema alcanzó un máximo de 450w de consumo de energía" causa pánico, mientras que nadie se da cuenta de que el sistema "optimizado", aunque solo alcanzó un máximo de 183w, tardó 5 veces más en funcionar y consumió un total de vatios/hora más alto.El código no se muestra correctamente debido a una confusión de HTML.¡Pero al menos los corchetes curley están en el lugar correcto!Arreglado, creo.Hay algo en WordPress donde a veces haces una edición y hace esto.Sé que cualquier deshacer en el documento lo hace.Entonces, en algún momento entre que lo introduje y lo publiqué, alguien hizo una de las cosas mágicas que hacen que WordPress se vuelva loco y ahí estás... Lo siento.bifurcar sigue siendo la forma de Linux de hacer subprocesos.Una buena práctica es usar async para cargas de E/S y bifurcaciones para carga de CPU.Actualmente trabajo en una compañía de Windows y también usan subprocesos para E/S, lo cual no está bien.La aceleración también es limitada. Consulte la ley de Amdahl.https://en.wikipedia.org/wiki/Amdahl%27s_lawEl uso de subprocesos para E/S está perfectamente bien, ya que son muy livianos, aunque no tienen el mismo rendimiento que las llamadas a procedimientos asincrónicos o las E/S superpuestas.En mi opinión, eso depende en gran medida de la cantidad de memoria que tiene que copiar el subproceso y de cómo lo comparte y lo bloquea.Con E/S masiva (por ejemplo, servidores web apache vs ngix), es posible que entre en un infierno trillador o necesite hacer un uso inteligente de los cachés.Además, las implementaciones sin bloqueo son más pesadas que "simplemente" usar cosas normales.Mutexes y semáforos también consumen memoria.Entonces depende de su modelo de roscado.Si es espacio de usuario, puede salirse con la suya.La agrupación de memoria también podría ser una buena idea en tales casos.En mi opinión, hacer I/O async es mucho más fácil y ligero.Con el azúcar sintáctico como asíncrono, tampoco es un "infierno" de devolución de llamada. Pero bueno, acepto su respuesta porque eso es lo que mis universidades me dicen todo el tiempo.C ++ no implementaría asíncrono en hilos si eso perjudicara a alguien.(Bueno, eso no es cierto en todos los casos, ya que, dependiendo de la implementación, tiene un grupo de subprocesos)Depende mucho del tipo de E/S.Si tiene, digamos, una máquina de 4 núcleos con solo 10 000 conexiones, un subproceso para cada conexión, con su propia pila de quizás 2 MB, usará 20 GB de memoria, solo para realizar un seguimiento de las conexiones.El uso de un subproceso de sondeo y el servicio asincrónico de conexiones con un grupo de trabajadores de 4 subprocesos es usar 5 subprocesos * 2 MB = solo 10 MB de memoria.Al atender menos conexiones de alta demanda donde el uso de ancho de banda, disco o CPU es el límite, sí, los subprocesos serán tan buenos como un grupo de trabajadores asíncronos.También me gusta 'fork'ing, cada vez que hay oportunidad.Pero los procesos 'hijos' consumen todos mis recursos.(léase 'comestibles'.Entonces, el servidor tiene 32 núcleos y 100000 mips y todavía no es suficiente para ejecutar los programas en esos libros de texto de programación de 1985 y, sin embargo, de alguna manera funcionaron muy bien en un Sun 3.Mis procesos 'hijos' también consumen mi tiempo y dinero...Tuve que portar una gran aplicación de servidor Unix a Windows NT en los años 90.La falta de una bifurcación () en Windows me causó todo tipo de problemas.Windows tenía "spawn", que era como una combinación de fork() y exec().Pero un programa en particular escucharía las conexiones y fork() para cada conexión sin exec().Miré cómo Cygwin (entonces llamado gnuwin32) implementó fork() en Windows y fue bastante horrible.Así que terminé dividiendo el programa en dos partes... un oyente y un corredor.La respuesta de cygwin era necesaria pero si… Verdaderamente feo.Tuve la misma experiencia.Creo que este artículo debería haberse titulado "cómo usar un martillo en una nave estelar", es decir, es mucho mejor, y lo ha sido durante muchas décadas, escribir su programa como uno multiproceso adecuado al principio...Depende mucho.Uno no va diseñando un núcleo warp en una baliza de navegación.Complica excesivamente la baliza e introduce modos de falla innecesarios, incluidas oportunidades para la sensibilidad.El uso original, con diferentes informes generados para diferentes impresoras, es un buen caso de uso para fork().Pero en el ejemplo del promedio de matriz, sería mucho más simple usar openmp, que está integrado en las versiones modernas de gcc.Simplemente precede el ciclo for que llena la matriz con:y el bucle for que suma los valores con:#pragma omp paralelo para reducción(+ : total)y luego dé una opción -fopenmp (o equivalente) al compilador.El resultado es un código más ordenado, con suma automática de los totales en todos los subprocesos sin tener que escribir ningún código de comunicación entre subprocesos, el compilador/biblioteca se encarga de calcular cuántos subprocesos iniciar en el procesador específico y el código tener compatibilidad retroactiva de un solo subproceso con compiladores que no son compatibles con openmp.(La desventaja del enfoque openmp aquí es que si algunos núcleos terminan su parte de llenar la matriz, permanecerán inactivos hasta que los otros núcleos terminen esta parte de su tarea antes de comenzar con la suma).Las personas que pueden hacer estas cosas nunca dejan de sorprenderme y asombrarme.Ciencia espacial en lo que a mí respecta.Supongo que esto le da un nuevo significado a "Ponle un tenedor, está hecho"No hay nada extraño en usar una llamada al sistema para hacer aquello para lo que está diseñada.Bueno, no hay nada 'hack'y sobre la mayoría de las cosas en hack a day.Así que... a la par del curso realmente...La mayoría de las bibliotecas de Linux (sobre todo glibc) han estado usando clone/clone2/clone3 en lugar de fork() durante mucho tiempo.Cierto, aunque no olvides la capacidad de disipación de calor.Gran parte de la innovación en el diseño de chips se centra en los procesadores móviles que pueden exceder su presupuesto térmico y tienen que acelerar antes de completar la tarea con el vataje más alto.…y, por supuesto, siempre están esos desarrolladores web idiotas a los que les encanta asumir que pueden engullir la CPU sostenida hasta que las vacas vuelvan a casa porque nadie ha aprobado ningún tipo de ley para devolver el costo de quemar el tiempo de CPU del cliente a las empresas.Puaj.Olvidé que WordPress dice "respondiendo a..." pero en realidad no registra esa información si tiene JavaScript deshabilitado.Estaba respondiendo a https://hackaday.com/2022/04/21/linux-fu-an-odd-use-for-fork/#comment-6464093Sea amable y respetuoso para ayudar a que la sección de comentarios sea excelente.(Política de comentarios)Este sitio utiliza Akismet para reducir el spam.Aprende cómo se procesan los datos de tus comentarios.Al utilizar nuestro sitio web y nuestros servicios, usted acepta expresamente la colocación de nuestras cookies de rendimiento, funcionalidad y publicidad.Aprende más